Since .NET Framework 3.5, developers have been able to add extension methods callable from instances of any object type. Extension properties have not been implemented in C#, however. Unlike extension methods, extension properties would involve storing some extra state information for individual objects.
However, even for extension methods, it would be highly useful in some programming scenarios to be able to access after-added/extension information for the objects on which those extension methods are called.
Here is the original question: How can one add extension properties, or otherwise set extended data on an object in C#?
Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are static methods, but they're called as if they were instance methods on the extended type.
The main advantage of the extension method is to add new methods in the existing class without using inheritance. You can add new methods in the existing class without modifying the source code of the existing class. It can also work with sealed class.
Extension methods, introduced in Dart 2.7, are a way to add functionality to existing libraries. You might use extension methods without even knowing it. For example, when you use code completion in an IDE, it suggests extension methods alongside regular methods.
There's no way to add them currently because the feature doesn't exist in C#.
The System.Runtime.CompilerServices.ConditionalWeakTable class seems to be just what the doctor ordered, and doesn't seem to entail the sort of memory leak worries that other approaches might raise. Following is my first simple wrapper around the use of ConditionalWeakTable. I will hide them a bit better (make them internal and more obscurely named) and put other methods in front of them, but this works and is a big relief and help to me.
(Thanks to svick, Jeppe Stig Nielsen, Tormod, and user2246674 for helping me think this through.)
public static class ExtensionMethods
{
private static System.Runtime.CompilerServices.ConditionalWeakTable<object, object> extendedData = new System.Runtime.CompilerServices.ConditionalWeakTable<object, object>();
internal static IDictionary<string, object> CreateDictionary(object o) {
return new Dictionary<string, object>();
}
public static void SetExtendedDataValue(this object o, string name, object value) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, ExtensionMethods.CreateDictionary);
// if (values == null)
// extendedData.Add(o, values = new Dictionary<string, object>()); // This doesn't seem to be necessary!
if (value != null)
values[name] = value;
else
values.Remove(name);
}
public static T GetExtendedDataValue<T>(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, ExtensionMethods.CreateDictionary);
// if (values == null) // ... nor does this!
// return default(T);
// else
if (values.ContainsKey(name))
return (T)values[name];
else
return default(T);
}
internal static object GetExtendedDataValue(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, null);
if (values == null)
return null;
else if (values.ContainsKey(name))
return values[name];
else
return null;
}
}
(EDIT: The original answer follows, for historical purposes.)
The System.ComponentModel.TypeDescriptor.GetAttributes(object) method exposes a collection of System.Attribute objects that have been added to the specified object. Thus if an attribute were added to an object (but not a struct or enum), capable of storing key-value pairs, those pairs could be accessed via extension methods, hiding the storage mechanism from calling code. This is not quite as clean as extension properties would be, because of the unavoidable method pcall syntax, but is still useful in certain programming scenarios.
Since the object storing the data must inherit from System.Attribute, and it is unknown in advance what type of data will need to be stored, a straightforward solution is to create a class that both inherits from System.Attribute and implements IDictionary. Easy-to-use extension methods can then be made to wrap the use of this class, simplifying further the storage and retrieval of extension data.
Following is code for one such implementation:
/// <summary>
/// A System.Attribute which is also an IDictionary, useful for adding extension data to
/// individual objects, no matter the type
/// </summary>
public class ExtensionDataAttribute : System.Attribute, IDictionary<string, object>
{
// The dictionary wrapped by this collection, which cannot extend by System.Attribute and Dictionary at once
private IDictionary<string, object> data = new Dictionary<string, object>();
/// <summary>
/// Adds this collection of extension data to the specified object; should be called only once
/// </summary>
/// <param name="o">The object to which to add this collection of extension data</param>
public void AddTo(object o) {
System.ComponentModel.TypeDescriptor.AddAttributes(o, this);
}
// Following are encapsulated calls to the wrapped dictionary, which should need no explanation;
// after accessing an ExtensionDataAttribute instance, simply use it as an IDictionary<string, object>
public void Add(string key, object value)
{
data.Add(key, value);
}
public bool ContainsKey(string key)
{
return data.ContainsKey(key);
}
public ICollection<string> Keys
{
get { return data.Keys; }
}
public bool Remove(string key)
{
return data.Remove(key);
}
public bool TryGetValue(string key, out object value)
{
return data.TryGetValue(key, out value);
}
public ICollection<object> Values
{
get { return data.Values; }
}
public object this[string key]
{
get
{
return data[key];
}
set
{
data[key] = value;
}
}
public void Add(KeyValuePair<string, object> item)
{
data.Add(item);
}
public void Clear()
{
data.Clear();
}
public bool Contains(KeyValuePair<string, object> item)
{
return data.Contains(item);
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
data.CopyTo(array, arrayIndex);
}
public int Count
{
get { return data.Count; }
}
public bool IsReadOnly
{
get { return data.IsReadOnly; }
}
public bool Remove(KeyValuePair<string, object> item)
{
return data.Remove(item);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return data.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return data.GetEnumerator();
}
}
... and some generic extension methods to wrap it up a bit further:
/// <summary>
/// Extension methods for setting and getting extension data for individual objects, no matter the type
/// </summary>
public static class ExtensionDataAttributeExtensions {
public static void SetExtensionDataAttributeValue(this object o, string name, object value)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
{
((ExtensionDataAttribute)a)[name] = value;
return;
}
ExtensionDataAttribute extensionData = new ExtensionDataAttribute();
extensionData[name] = value;
extensionData.AddTo(o);
}
public static T GetExtensionDataAttributeValue<T>(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
return (T)((ExtensionDataAttribute)a)[name];
return default(T);
}
public static object GetExtensionDataAttributeValue(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
return ((ExtensionDataAttribute)a)[name];
return null;
}
public static void RemoveExtensionDataAttributeValue(this object o, string name) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
((ExtensionDataAttribute)a).Remove(name);
}
}
... and, finally, two examples of custom extension methods to use this idea in real-world code. One uses the ExtensionDataAttribute class directly (and is thus a little more nuts-and-bolts), the other uses the generic extension methods provided above:
/// <summary>
/// Extension methods showing samples of using the ExtensionDataAttribute class directly, for use
/// in situations where it is undesirable to include the extension methods provided with that class
/// </summary>
public static class ExtensionMethodsExample1 {
/// <summary>
/// Adds a description to the specified string object
/// </summary>
/// <param name="s">The string to describe</param>
/// <param name="description">The description to set</param>
public static void SetDescription(this string s, string description) {
if (string.IsNullOrWhiteSpace(description))
description = "";
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(s))
if (a is ExtensionDataAttribute) {
((ExtensionDataAttribute)a)["Description"] = description;
return;
}
ExtensionDataAttribute extensionData = new ExtensionDataAttribute();
extensionData["Description"] = description;
extensionData.AddTo(s);
}
/// <summary>
/// Gets the description for the specified string, if it has one;
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static string GetDescription(this string s) {
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(s))
if (a is ExtensionDataAttribute) {
ExtensionDataAttribute eda = (ExtensionDataAttribute)a;
if (eda.ContainsKey("Description"))
return eda["Description"].ToString();
else
return "";
}
return "";
}
}
/// <summary>
/// Extension methods encapsulating calls to extension methods provided with the ExtensionDataAttribute
/// class, demonstrating increased ease of implementing one's own extension data
/// </summary>
public static class ExtensionMethodsExample2 {
public static string GetDescription(this string s)
{
return s.GetExtensionDataAttributeValue<string>("Description");
}
public static void SetDescription(this string s, string description)
{
s.SetExtensionDataAttributeValue("Description", description);
}
}
I hope that these ideas have been useful. One doesn't always have the luxury of extending a class, and in some situations it may make the design of extension methods cleaner if one does not have to assemble and pass in extra information with each method call, on an object which may not have been created in the developer's codebase at all.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With