DEV Community

Zachary Patten
Zachary Patten

Posted on

Value-Based Attributes in C#

Value-Based Attributes in C Sharp

This code is a snippet from the https://github.com/ZacharyPatten/Towel project.
Please check out the project if you want to see more code like it. :)

Attributes in C# are great. They let you add metadata onto members of your code that can be dynamically looked up at runtime. However, they are also a bit of a pain to deal with. You generally have to make your own class that inherits System.Attribute and add a decent bit of boiler plate code to look up your attribute with reflection. Here is an example of making your own attribute:

using System;
using System.Linq;
using System.Reflection;

static class Program
{
    static void Main()
    {
        MyAttribute attribute = typeof(MyClass).GetCustomAttributes<MyAttribute>().FirstOrDefault();
        Console.WriteLine("MyCustomAttribute...");
        if (!(attribute is null))
        {
            Console.WriteLine("Found: " + true);
            Console.WriteLine("Value: " + attribute.Value);
        }
        else
        {
            Console.WriteLine("Found: " + false);
            Console.WriteLine("Value: " + null);
        }
    }
}

public class MyAttribute : Attribute
{
    public string Value { get; private set; }

    public MyAttribute(string value)
    {
        Value = value;
    }
}

[MyAttribute("hello world")]
public class MyClass { }
Enter fullscreen mode Exit fullscreen mode

Sure... it's not a ton of boiler plate... but it is still annoying... so I made a value-based attribute so that I can use constant values rather than having to define a new attribute type:

using System;
using Towel;

static class Program
{
    static void Main()
    {
        var (Found, Value) = typeof(MyClass).GetValueAttribute("MyCustomAttribute");
        Console.WriteLine("MyCustomAttribute...");
        Console.WriteLine("Found: " + Found);
        Console.WriteLine("Value: " + Value);
    }
}

[Value("MyCustomAttribute", "hello world")]
public class MyClass { }
Enter fullscreen mode Exit fullscreen mode

The first parameter of the ValueAttribute is like a key while the second parameter of the
attribute is the actual value. Then you just use the GetValueAttribute extension method with the key (first parameter) and it will do the reflection for you. You can add multiple ValueAttributes per code member:

[Value("Name", "Array Benchmarks")]
[Value("Description", "These benchmarks do...")]
[Value("Source URL", "google.com")]
public class MyBenchmarks
{
    // code...
}
Enter fullscreen mode Exit fullscreen mode

How does it work? It does exactly the same thing as the first example, but it just looks for
the the ValueAttribute that contains the matching key value. Here is the source code:

using System;
using System.Reflection;

namespace Towel
{
    /// <summary>A value-based attribute.</summary>
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
    public class ValueAttribute : Attribute
    {
        internal object Attribute;
        internal object Value;

        /// <summary>Creates a new value-based attribute.</summary>
        /// <param name="attribute">The attribute.</param>
        /// <param name="value">The value.</param>
        public ValueAttribute(object attribute, object value)
        {
            Attribute = attribute;
            Value = value;
        }
    }

    /// <summary>Extension methods for reflection types and <see cref="ValueAttribute"/>.</summary>
    public static class ValueAttributeExtensions
    {
        /// <summary>Gets a <see cref="ValueAttribute"/> on a <see cref="MemberInfo"/>.</summary>
        /// <param name="memberInfo">The type to get the <see cref="ValueAttribute"/> of.</param>
        /// <param name="attribute">The attribute to get the value of.</param>
        /// <returns>
        /// (<see cref="bool"/> Found, <see cref="object"/> Value)
        /// <para>- <see cref="bool"/> Found: True if the attribute was found; False if not or if multiple attributes were found (ambiguous).</para>
        /// <para>- <see cref="object"/> Value: The value if found or default if not.</para>
        /// </returns>
        public static (bool Found, object Value) GetValueAttribute(this MemberInfo memberInfo, object attribute)
        {
            _ = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
            bool found = false;
            object value = default;
            foreach (ValueAttribute valueAttribute in memberInfo.GetCustomAttributes<ValueAttribute>())
            {
                if (ReferenceEquals(attribute, valueAttribute.Attribute) || attribute.Equals(valueAttribute.Attribute))
                {
                    if (found)
                    {
                        return (false, default);
                    }
                    found = true;
                    value = valueAttribute.Value;
                }
            }
            return (found, value);
        }

        /// <summary>Gets a <see cref="ValueAttribute"/> on a <see cref="ParameterInfo"/>.</summary>
        /// <param name="parameterInfo">The type to get the <see cref="ValueAttribute"/> of.</param>
        /// <param name="attribute">The attribute to get the value of.</param>
        /// <returns>
        /// (<see cref="bool"/> Found, <see cref="object"/> Value)
        /// <para>- <see cref="bool"/> Found: True if the attribute was found; False if not or if multiple attributes were found (ambiguous).</para>
        /// <para>- <see cref="object"/> Value: The value if found or default if not.</para>
        /// </returns>
        public static (bool Found, object Value) GetValueAttribute(this ParameterInfo parameterInfo, object attribute)
        {
            _ = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo));
            bool found = false;
            object value = default;
            foreach (ValueAttribute valueAttribute in parameterInfo.GetCustomAttributes<ValueAttribute>())
            {
                if (ReferenceEquals(attribute, valueAttribute.Attribute) || attribute.Equals(valueAttribute.Attribute))
                {
                    if (found)
                    {
                        return (false, default);
                    }
                    found = true;
                    value = valueAttribute.Value;
                }
            }
            return (found, value);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (4)

Collapse
 
entomy profile image
Patrick Kelly

While the implementation is handy, the naming doesn't fit with established conventions nor what this is doing. "value attribute" proposes the attribute is a value type, as is the case with ValueTuple, ValueTask, ValueStringBuilder, and more. What you have, is an attribute defining key-value pairs: an association.

Collapse
 
zacharypatten profile image
Zachary Patten

Agreed. I couldn't think of a name... if you have ideas I'm all ears. :) I don't really like ValueAttribute

Collapse
 
entomy profile image
Patrick Kelly

First thing that comes to mind is "data". It's general enough and makes sense for what is a "data entry". If I think of anything more clear I'll let you know.

Thread Thread
 
zacharypatten profile image
Zachary Patten

Another user sugguested TagAttribute so a use case would be [Tag("Name", "My Name")] and the extension method could just be GetTag rather than GetTagAttribute as GetTag is probably clear enough what it is doing. I think I decided to go with that and will update the code and this article when available.