Skip to content

Instantly share code, notes, and snippets.

@ZacharyPatten
Last active September 21, 2020 01:35
Show Gist options
  • Save ZacharyPatten/2713e11a2bb1806a627e00b16a9ddb85 to your computer and use it in GitHub Desktop.
Save ZacharyPatten/2713e11a2bb1806a627e00b16a9ddb85 to your computer and use it in GitHub Desktop.
Valuue-Based Attributes in C#

Value-Based Attributes in C#

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);
		}
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment