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 { }
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 { }
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...
}
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);
}
}
}
Top comments (4)
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.
Agreed. I couldn't think of a name... if you have ideas I'm all ears. :) I don't really like
ValueAttribute
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.
Another user sugguested
TagAttribute
so a use case would be[Tag("Name", "My Name")]
and the extension method could just beGetTag
rather thanGetTagAttribute
asGetTag
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.