Desirable Attributes

by casperOne 10. February 2009 18:41

The documentation for .NET Framework Core Development is a little thin when it comes to guidance on writing custom attributes.  Attributes are classes (just like everything else), and in that sense all of the framework documentation on designing classes is still very relevant.  Attributes however, play an important role in that they are part of the static definition of the assembly/type/member that they are applied to.

At its core, .NET is a statically-typed runtime (although .NET 4.0 will change that with the introduction of the Dynamic Language Runtime).  Types are verified at compile-time and their definition remains constant throughout the lifetime of any application domain that they are loaded into (yes, there are dynamic types, but once those types are defined, those definitions are static as well).

Given that, it comes as no surprise that the following code doesn’t compile:

using System;
using System.Reflection;

namespace DesirableAttributes
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Get the type of the person.
            Type personType = typeof(Person);

            // Get the age property.
            PropertyInfo agePropertyInfo = personType.GetProperty("Age");

            // Try to change the property to be of type string.
            agePropertyInfo.PropertyType = typeof(string);
        }
    }
}

The C# compiler outputs the error “Property or indexer 'System.Reflection.PropertyInfo.PropertyType' cannot be assigned to -- it is read only”.  And true to its word, the PropertyType property is read-only.  But if you stop to think about why it is read-only, it is the manifestation of the fact that .NET is a statically-typed system; changing the definition of the type at runtime would violate basic guarantees that are made by the CLR.

That being said, the following is permissible:

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

namespace DesirableAttributes
{
    [AttributeUsage(AttributeTargets.All)]
    public class HelpAttribute : Attribute
    {
        /// <summary>The url where the documentation 
        /// for the member is located.</summary>
        public string DocumentationUrl { get; set; }
    }
    
    [Help(DocumentationUrl="http://www.tempuri.org/Person.html")]
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Get the type of the person.
            Type personType = typeof(Person);

            // Get the help attribute attached to the Person class.
            HelpAttribute helpAttribute = GetHelpAttribute(personType);

            // Outputs:
            //
            // Documentation Url Before: http://www.tempuri.org/Person.html
            Console.WriteLine("Documentation Url Before: {0}", helpAttribute.DocumentationUrl);

            // Change the type definition.
            helpAttribute.DocumentationUrl = "http://www.malicious.org/Person.html";

            // Outputs:
            //
            // Documentation Url After: http://www.malicious.org/Person.html
            Console.WriteLine("Documentation Url After: {0}", helpAttribute.DocumentationUrl);
        }

        static HelpAttribute GetHelpAttribute(Type type)
        {
            // Get the help attribute attached to the Person class.
            return type.GetCustomAttributes(typeof(HelpAttribute), false).
                Cast<HelpAttribute>().FirstOrDefault();
        }
    }
}

In the code sample above, the property on the attribute is permitted to be changed, effectively changing the type definition as long as that instance of the attribute is used.  This can cause a problem if instances of an attribute with read/write properties are held for extended periods of time.

Fortunately, the CLR does not let us down completely, in that further calls to GetCustomAttributes will not persist the property values that are changed on attributes:

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

namespace DesirableAttributes
{
    [AttributeUsage(AttributeTargets.All)]
    public class HelpAttribute : Attribute
    {
        /// <summary>The url where the documentation 
        /// for the member is located.</summary>
        public string DocumentationUrl { get; set; }
    }
    
    [Help(DocumentationUrl="http://www.tempuri.org/Person.html")]
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Get the type of the person.
            Type personType = typeof(Person);

            // Get the help attribute attached to the Person class.
            HelpAttribute helpAttribute = GetHelpAttribute(personType);

            // Outputs:
            //
            // Documentation Url Before: http://www.tempuri.org/Person.html
            Console.WriteLine("Documentation Url Before: {0}", helpAttribute.DocumentationUrl);

            // Change the type definition.
            helpAttribute.DocumentationUrl = "http://www.malicious.org/Person.html";

            // Get the attribute again.
            helpAttribute = GetHelpAttribute(personType);

            // Outputs:
            //
            // Documentation Url After: http://www.tempuri.org/Person.html
            Console.WriteLine("Documentation Url After: {0}", helpAttribute.DocumentationUrl);
        }

        static HelpAttribute GetHelpAttribute(Type type)
        {
            // Get the help attribute attached to the Person class.
            return type.GetCustomAttributes(typeof(HelpAttribute), false).
                Cast<HelpAttribute>().FirstOrDefault();
        }
    }
}

The output from the code sample above shows that calling GetCustomAttributes will return different instances on each call.

So what can we gather from this?

The first thing that we can gather from this is the following tenant:

DO NOT cache instances of attributes returned from calls to GetCustomAttributes.

But what about our own attributes?  What can we do to ensure that clients of attributes that we define are not changed mistakenly or maliciously?

MSIL allows for attributes to have properties on values set in the application of the attribute.  C# has syntax in the language to realize this.  Recall the earlier code sample:

[Help(DocumentationUrl="http://www.tempuri.org/Person.html")]
public class Person

Note how the DocumentationUrl property is used to specify which property is being set.  While it is a nice, simple syntax for assigning attributes, it requires the property on the attribute to have read/write accessors, which presents the original issue of allowing the type to be mutated.

This leads us to the following tenants when creating our own attributes:

DO NOT expose properties on attributes that have read AND write accessors.

DO expose properties on attributes that are read-only.

DO expose constructors that take as parameters all the values that will be exposed through the read-only properties on the attribute.

With those in mind, the HelpAttribute would be redefined as:

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : Attribute
{
    public HelpAttribute(string documentationUrl)
        : base()
    {
        // Set the documentationUrl.
        this.documentationUrl = documentationUrl;
    }

    string documentationUrl;

    /// <summary>The url where the documentation 
    /// for the member is located.</summary>
    public string DocumentationUrl { get { return documentationUrl; } }
}

And applying it to a type would change to:

[Help("http://www.tempuri.org/Person.html")]
public class Person

In defining the attribute this way, the attribute (and by extension, the assembly/type/member it is applied to) would not change over the life of the application domain, enforcing the ideal of a statically-typed environment.

But what are the drawbacks?

(Note: None of the code samples from this point on will compile, as they are all recommendations for the C# language)

The drawback here is that for attributes with a large number of parameters, you would have to define a constructor with a large number of parameters, which can become unwieldy.  Unfortunately, there currently isn’t a solution to this right now.

To address this, I would recommend having a class who’s sole purpose is to pass values to the constructor of the attribute:

public class HelpAttributeValues
{
    public string DocumentationUrl { get; set; }
}

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : Attribute
{
    public HelpAttribute(HelpAttributeValues values)
        : base()
    {
        // Set the values.
        this.values = values;
    }

    HelpAttributeValues values;

    /// <summary>The url where the documentation 
    /// for the member is located.</summary>
    public string DocumentationUrl { get { return values.DocumentationUrl; } }
}

And then to apply it, one would use the new object initializer syntax introduced in C# 3.0 to give the appearance of named parameters:

[Help(new HelpAttributeValues() { 
    DocumentationUrl = "http://www.tempuri.org/Person.html" })]
public class Person

Combine this with my recommendation for enhancements to the new keyword, and it could be shortened to this:

[Help(new { DocumentationUrl = "http://www.tempuri.org/Person.html" })]
public class Person

Tags: , ,

programming