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 (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 (attribute.Equals(valueAttribute.Attribute))
{
if (found)
{
return (false, default);
}
found = true;
value = valueAttribute.Value;
}
}
return (found, value);
}
}
}