Using .NET Custom Attributes for release documentation

During the last Dev Days in the Netherlands I’ve attended to Francesco Balena's presentation on .NET Reflection. It was definitely interesting and it has inspired me to take a closer look at it. After a while I came on the idea of using .NET Reflection for generating release documentation for .NET applications and assemblies. It would once and for all solve the problem of updating the release notes stored in a separate file and getting sure all the changes have been put there.

I would like to show you the way I’ve put my idea into practice and how you could take it even further.

My demo CustomAttributes solution consists of three projects:

  • CustomAttributes – definition of the custom attributes
  • CustomAttributesClient – a Windows Forms Application generating the release notes using the .NET reflection
  • MyAssembly – a dummy assembly that makes use of the custom attributes should resemble your assemblies

dotNETCustomAttributesRelease1

First of all I’ve created the CustomAttributes project. I’ve started with creating two custom attributes: Change (for keeping changes) and Features (for marking extra functionality added to the assembly in the given release). Then I thought that it would be nice to store the bug fixes as well. As a bug fix is nothing more than a change I’ve decided to create a ChangeBase abstract class and make the Change and BugFix classes derive from it. For clarity I’ve stored each custom attribute in a separate class file.

After defining the attributes I moved to setting the attributes. I’ve decided to store a date and a description of each change – no matter if it’s a Change or a BugFix. As we all use unique initials here at Imtech ICT Business Solutions I’ve added an optional Named Parameter to the ChangeBase class.

public abstract class ChangeBase : Attribute  
{
  protected DateTime _Date;
  protected string _Change;
  private string _Initials;

  public string Initials
  {
    get { return _Initials; }
    set { _Initials = value.ToUpper(); }
  }

  public ChangeBase(string changeDate, string changeDescription)
  {
    _Date = DateTime.Parse(changeDate);
    _Change = changeDescription;
  }
}

Then I’ve moved on to defining the Change and BugFix attributes. Each custom attribute has to have the usage scope defined. As a change or a bug fix can apply almost to any part of a assembly I’ve decided to set the scope to AttributeTargets.All. Because I want to keep the history of the changes I’ve set the AllowMultiple attribute to true.

Here are both the attributes:

[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class ChangeAttribute : ChangeBase  
{
  public DateTime Date
  {
    get { return base._Date; }
  }

  public string Change
  {
    get { return base._Change; }
  }

  public ChangeAttribute(string changeDate, string changeDescription)
    : base(changeDate, changeDescription)
  {
  }
}

And the BugFix:

[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class BugFixAttribute : ChangeBase  
{
  public DateTime BugFixDate
  {
    get { return base._Date; }
  }

  public string BugFixDescription
  {
    get { return base._Change; }
  }

  public BugFixAttribute(string fixDate, string fixDescription)
    : base(fixDate, fixDescription)
  {

  }
}

For clarity I’ve given new names to the date and description properties in the BugFix attribute. Then I’ve moved to the Feature attribute. First of all the attributes: I definitely wanted to store the name of each feature and the version of the release it came out with. Next to these I’ve set some optional space for extra description if needed. As for setting the attribute scope I’ve chosen for class, method and enumeration only: extra functionality is mostly a bigger chunk of code. Initially I’ve been using class and method only. Recently however I’ve faced expanding one of my assemblies with a quite important enumeration, so I’ve decided to extend the Feature attribute with the enumeration scope as well. So here is how the Feature attribute looks like:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method |
  AttributeTargets.Enum, AllowMultiple = false)]
public class FeatureAttribute : Attribute  
{
  private string _Name;
  public string Name
  {
    get { return _Name; }
  }

  private Version _AddedInVersion;
  public Version AddedInVersion
  {
    get { return _AddedInVersion; }
  }

  private string _Description;
  public string Description
  {
    get { return _Description; }
    set { _Description = value; }
  }

  public FeatureAttribute(string featureName, string addedInVersion)
  {
    _Name = featureName;
    _AddedInVersion = new Version(addedInVersion);
  }
}

At this moment we have defined three custom attributes we can use for storing the changes within our assemblies and using them for generating release documentation. Below you can see an exemplary assembly making actual use of the defined custom attributes:

public class ClassFoo  
{
  public ClassFoo()
  {

  }

  [Feature("Sum of n-integers", "1.0")]
  public static int Sum(params int[] numbers)
  {
    int sum = 0;

    foreach (int i in numbers)
      sum += i;

    return sum;
  }

  [Feature("Sum of n-doubles", "1.1")]
  public static double Sum(params double[] numbers)
  {
    double sum = 0.0;

    foreach (double d in numbers)
      sum += d;

    return sum;
  }

  [Feature("Average of n-integers", "1.1")]
  public static double Avg(params int[] numbers)
  {
    if (numbers.Length > 0)
    {
      double avg = Sum(numbers) / numbers.Length;

      return avg;
    }
    else
      return 0;
  }

  [Feature("Average of n-doubles", "1.1")]
  [Change("2007-09-16", "Exception if no numbers passed")]
  [BugFix("2007-09-17", "Diving by 0 check implemented")]
  public static double Avg(params double[] numbers)
  {
    if (numbers.Length > 0)
    {
      double avg = Sum(numbers) / numbers.Length;
      return avg;
    }
    else
      throw new ArgumentException
        ("You haven't passed any numbers to this method");
  }
}

The assembly doesn’t do anything spectacular actually: just a few simple methods. What I would like to focus on is the usage of the attributes.

Let’s proceed with the last part of the solution: release notes generator. I’ve use for this purpose a simple Windows Forms Application with a button, a label (for displaying the file name), a Multiline TextBox for displaying the generated documentation and an OpenFileDialog for choosing the assembly: nothing spectacular so far. First of all I initiate the OpenFileDialog and let the user pick an assembly:

if (openFileDialog.ShowDialog() == DialogResult.OK)  
{
  OpenFileLabel.Text = openFileDialog.FileName;

  GenerateReleaseNotes();
}

Then we move to the core:

private void GenerateReleaseNotes()  
{
  Assembly a = Assembly.LoadFile(openFileDialog.FileName);

  foreach (Module m in a.GetModules())
  {
    //InfoTextBox.Text += m.Name + "\r\n";
    foreach (Type t in m.GetTypes())
    {
      //InfoTextBox.Text += t.Name + "\r\n";
      foreach (MethodInfo mi in t.GetMethods())
      {
        if (mi.GetCustomAttributes(typeof(FeatureAttribute),
          true).Length > 0)
        {
          //InfoTextBox.Text += mi.Name + "\r\n";
          foreach (object o in mi.GetCustomAttributes(
            typeof(FeatureAttribute), true))
          {
            FeatureAttribute fa = (FeatureAttribute)o;
            if (!features.ContainsKey(fa.AddedInVersion))
              features.Add(fa.AddedInVersion, 
                new List<FeatureAttribute>());
            features[fa.AddedInVersion].Add(fa);
          }
        }
      }
    }
  }

  FileVersionInfo vi = System.Diagnostics.FileVersionInfo.
    GetVersionInfo(openFileDialog.FileName);
  InfoTextBox.Text = String.Format("{0} v{1} by {2}\r\n",
    vi.ProductName, vi.ProductVersion, vi.CompanyName);

  InfoTextBox.Text += "Available Features:\r\n\r\n";

  foreach (KeyValuePair<Version, List<FeatureAttribute>> kvp
    in features)
  {
    InfoTextBox.Text += "v" + kvp.Key.ToString() + ":\r\n";
    foreach (FeatureAttribute fa in kvp.Value)
    {
      InfoTextBox.Text += "  - " + fa.Name + "\r\n";
    }
    InfoTextBox.Text += "\r\n";
  }
}

First of all we open the assembly using the full file name and path stored in the OpenFileDialog. You could load an assembly residing in the GAC as well. Then we step to processing the methods residing within modules in the selected assembly. In this example we’re focusing only on the methods: in the real life you should iterate through all the scope your attributes support to be sure you’ll pick all the Features-marked items within the assembly. In the mi.GetCustomAttributes() method we pass the FeatureAttribute as desired type. You could obtain all the available attributes as well but it would only complicate processing them as you would need to obtain the type of each attribute and process each of it separately. As we iterate through the found attributes, we cast each of them to the FeatureAttribute class. As we have the attribute casted we can use its properties straight forward.

As I’ve wanted to have all the features grouped per version and sorted descending on the version number, I’ve used the SortedDictionary generic collection with a custom comparer for the stored versions:

private SortedDictionary<Version, List<FeatureAttribute>> features =  
  new SortedDictionary<Version, List<FeatureAttribute>>(
    new VersionComparer());
class VersionComparer : IComparer<Version>  
{
  #region IComparer<Version> Members

  public int Compare(Version x, Version y)
  {
    return y.CompareTo(x);
  }

  #endregion
}

The comparer isn’t that complicated: it makes use of the standard Version comparer but then in the different order what gives us the desired result.

Now we move on to presenting the obtain data. As an extra I’ve obtained the product name, its version and the company name – all stored in the assembly. I did it using the FileVersionInfo class.

To display all the features within all the versions we will move iteratively through the features collection. We use the KeyValuePair generic for obtaining the combinations. Each pair consists of a Version (Key) and a List of Features (Value). It’s all pretty easy to use as all the properties are type safe – all thank to generics.

As we process our dummy assembly we get the following release notes:

dotNETCustomAttributesRelease2 

It’s not much but I think it’s a good beginning for extending the client application with other attributes we’ve just defined, extending the attributes with new properties or maybe even defining some new ones. The idea of keeping the documentation right next to the code works for me pretty well – no more extra documents and files!

As you might’ve noticed the GenerateReleaseNotes method contains some commented lines. You can check them out if you wish to get some more details on the way the assembly is actually being processed.

I hope I’ve inspired you to having a look at the demo project and having some more fun with the custom attributes. Let me know if you have any questions regarding this post or custom attributes in .NET.

You can download the solution I've used as well (Orcas format).

Comments

comments powered by Disqus