Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Demonstrating Custom Attributes: Build An Assembly Searching Tool.

0.00/5 (No votes)
25 Dec 2005 1  
Build an assembly searching system via custom attributes and reflection.

Introduction

Standard .NET attributes serve functionality similar to C++ preprocessor or pragma commands. They can be used to provide to the compiler additional information pertaining to some item in a program. They can be used to simplify certain programming tasks, e.g. setting security requirements declaratively (CodeAccessSecurityAttribute), controlling the physical layout of the data fields of a class or structure (StructLayoutAttribute), setting COM GUIDs to .NET classes (GuidAttribute), etc. They can even be used to deprecate items (ObsoleteAttribute). Attributes are no doubt a very useful .NET feature.

An even more useful feature is custom attributes by which developers can define and implement their own attributes. Just like their pre-defined counterparts, these user-defined attributes can be used to augment programmatic elements like classes, properties and methods with additional information which can be accessed by reflection.

Now, as we know, web pages are marked with keywords that can be picked up by the search engines. This enables us to easily search for web sites, documents and other resources using popular search engines like Google and Yahoo. Would it not be great if we could somehow search a machine for .NET assemblies which contain certain keywords? In this article, I aim to demonstrate how we can achieve this with the help of a custom attribute, and a folder-recursive assembly searching tool which we will develop ourselves.

I will present a sample implementation of a custom attribute named KeywordAttribute. The idea behind KeywordAttribute is taken directly from Adam Nathan's example attribute of the same name mentioned in his book ".NET and COM The Complete Interoperability Guide (SAMS Publishing)". Adam's idea is to provide a custom attribute which can used to mark .NET entities (classes, properties, methods, delegates, etc.) with searchable keywords plus a relevance level. This is similar in spirit to the way web pages are marked with keywords which are examined by search engines.

The following is an example use-case of KeywordAttribute:

[Keyword("Applies Keyword", 7), Keyword("MyClass", 10), Keyword("class")] 
public class MyClass 
{ 
    public MyClass() 
    { 
        ... 
        ... 
        ... 
    } 
}

Note that the KeywordAttribute attribute can be constructed by using a string parameter which indicates the keyword string. It can also be constructed using a keyword string together with an integer indicating the relevance value. If the relevance value is omitted, a default will be used.

In this article, I have opted to use Adam Nathan's entire KeywordAttribute class verbatim. Adam's implementation is already simple and elegant. In his book, Adam also provided an example of a browser application which can be used to list all keyword attributed objects contained inside an assembly. I have endeavored to exceed Adam's simple browser by building a complete assembly file searching system which is capable of recursively scanning through a file folder and picking up information on assemblies which contain types attributed by KeywordAttribute attributes which are parameterized by a specific keyword and a relevance value equal to or above a certain minimum.

I will assume that the reader has sufficient understanding of the .NET development work and usage of the C# language. Prior knowledge on custom attributes and reflection are also required.

General outline

The following is a general outline of how the main bulk of this article is organized:

  • The KeywordAttribute custom attribute

    We will examine Adam Nathan's implementation of this custom attribute. Much of Adam's original code as expounded in his book will be used. We will build a KeywordAttribute class library and enhance it with a strong name so that it can be registered into the GAC as a shared assembly and also to ensure that assemblies that use it can be registered to the GAC. Note that this class library is intrinsically useful and can be used in various projects.

  • The IDirectoryLister interface

    We begin our development of the assembly searching tool by defining an interface named IDirectoryLister. This interface defines structures, properties and methods which are to be implemented by a concrete class that provides the service of collating information on all files and sub-directories contained within a top-level folder.

  • The DirectoryListerBase class

    We will then build a concrete base class (DirectoryListerBase) in C# which implements the IDirectoryLister interface. This base class serves to provide workhorse functions which may be used by itself or be inherited by target-specific derived classes.

  • The AssemblyFilesLister class

    An example of a target-specific class derived from DirectoryListerBase is AssemblyFilesLister. This class builds upon the base functionality of DirectoryListerBase in order to emerge a specialized directory lister class that collects only assemblies. It is essentially a concrete implementation of the IDirectoryLister interface for assemblies. Both the DirectoryListerBase and AssemblyFilesLister classes are very useful generic classes which the reader can use in other projects.

  • The KeywordAttributeAssemblyLister application

    We will then create the KeywordAttributeAssemblyLister application which will use the services provided by the IDirectoryLister interface and the DirectoryListerBase and the AssemblyFilesLister classes. This application will provide further filtering services (exceeding that supplied by AssemblyFilesLister) by collecting only assemblies which use KeywordAttribute attributes parameterized by a specific keyword string and a relevance value equal to or above a certain minimum value.

  • Test keyword attributed assemblies

    I have pre-programmed several test assemblies which use the KeywordAttribute attribute (parameterized by a specific keyword named "test" and various relevance values). These assemblies will help us to test our KeywordAttributeAssemblyLister application.

Without further ado, let us begin by examining Adam Nathan's wonderful KeywordAttribute.

The KeywordAttribute custom attribute

The source code for the KeywordAttribute custom attribute is part of the KeywordAttribute.sln solution which is included in this article's source code zip file. Once unzipped, it can be found in the following directory: <main directory>\KeywordAttribute, where <main directory> is the location where you have unzipped the files. The source for the KeywordAttribute class is listed below:

using System; 
namespace KeywordAttributeNamespace 
{ 
    // Summary description for Keyword. 

    [AttributeUsage(AttributeTargets.All, AllowMultiple=true)] 
    public class KeywordAttribute : Attribute 
    { 
        private string m_strKeyword; 
        private int m_relevance; 
      
        public KeywordAttribute(string strKeyword) 
        { 
            m_strKeyword = strKeyword; 
            m_relevance = 5; 
        } 
        public KeywordAttribute(string strKeyword, int iRelevance) 
        { 
            m_strKeyword = strKeyword; 
            m_relevance = iRelevance; 
        } 
        public string Keyword 
        { 
            get { return m_strKeyword; } 
        } 
      
        // Property to get/set the relevance 

        public int Relevance 
        { 
            get { return m_relevance; } 
        } 
    } 
}

The Attribute class and the AttributeUsageAttribute

As you can see, it is very simple and elegant. The KeywordAttribute class is derived from the Attribute class and is itself attributed by the standard attributes AttributeUsageAttribute and AllowMultipleAttribute:

[AttributeUsage(AttributeTargets.All, AllowMultiple=true)]: These settings indicate that KeywordAttribute can be multiply applied to all programmatic entities which are:

  • Assembly
  • Class
  • Constructor
  • Delegate
  • Enum
  • Event
  • Field
  • Interface
  • Method
  • Module
  • Parameter
  • Property
  • ReturnValue
  • Struct

KeywordAttribute class members

The KeywordAttribute class contains two private member data:

  • private string m_strKeyword;
  • private int m_relevance;

These hold the specific keyword string and the relevance value.

Two constructors are defined: the first one takes only a keyword string (a relevance value of 5 will be used by default) and the second takes a keyword string and an integer indicating the relevance value. The KeywordAttribute class does not have a default constructor and must be instantiated by one of the parameterized constructors.

Get accessors are defined for the keyword string and the relevance value. No Set accessors are defined (these are not needed). The Get accessors are used during the Reflection process performed by the KeywordAttributeAssemblyLister application.

The KeywordAttribute assembly is strong named

The KeywordAttribute assembly is strong named. There are two important reasons for this:

  • The KeywordAttribute assembly is a shared assembly.

    Once an assembly uses the KeywordAttribute, the KeywordAttribute assembly becomes a dependency. Note that the KeywordAttributeAssemblyLister application itself references the KeywordAttribute hence the KeywordAttribute assembly is also a dependency of the KeywordAttributeAssemblyLister application.

    This being the case, the KeywordAttribute assembly referenced by the KeywordAttributeAssemblyLister application and by all assemblies that it loads (each of which potentially uses the KeywordAttribute) must be the same.

    This is because a .NET type's identity is associated with its containing assembly. Putting it another way, a containing assembly forms part of a .NET type's identity. Hence, if more than one assembly contains the definition of a type (even the exact same one), these are all considered separate and unrelated types to .NET. Hence, it matters which assembly a project references and loads at runtime when it needs a type definition.

    We therefore register the KeywordAttribute assembly to the GAC to ensure that it is a shared resource. I shall return to this point later when we will study the source codes of the KeywordAttributeAssemblyLister application.

  • Assemblies that use KeywordAttribute may want to be registered to the GAC.

    In order for the assemblies that use the KeywordAttribute be allowed to be registered into the Global Assembly Cache (GAC), the KeywordAttribute assembly itself must be compiled with a strong name.

A strong name key file has been pre-generated in the KeywordAttribute solution directory (keyfile.snk) and it is referenced in the KeywordAttribute solution's AssemblyInfo.cs file: [assembly: AssemblyKeyFile("..\\..\\keyfile.snk")].

Now that the KeywordAttribute assembly is strong named, it can also be registered into the GAC. This is done via the gacutil.exe tool. I have specially created a batch file RegisterAssemblyToGAC.bat that performs this action. Once the reader has compiled the KeywordAttribute solution, call this batch file to register the KeywordAttribute.dll into the GAC.

The IDirectoryLister interface

The assembly searching tool will be required to perform exhaustive and recursive searching through the sub-directories of a specified top-level directory. The target of the search is specifically assemblies which contain types that are attributed by the KeywordAttributes parameterized by a specific keyword string and a relevance value above or equal to a minimum.

Rather than developing a class that performs the above-mentioned specialized task, I have opted to elevate this operation into an abstract interface. This way, a client app which uses this interface may opt to select one of the several concrete implementations to realize a file searching operation.

We do this by defining an interface named IDirectoryLister. This interface defines structures, properties and methods which are to be implemented by a concrete class that provides the service of collecting information on all the files and sub-directories which are contained inside a top-level folder.

This interface is defined inside the DirectoryListerInterface.sln solution together with two associated structures. This solution is included in this article's source code zip file. Once unzipped, it can be found in the following directory: <main directory>\DirectoryLister\Interfaces\DirectoryListerInterface. The source is listed below:

using System; 
namespace DirectoryListerInterfaceNamespace 
{ 
  public struct FileEntry 
  { 
    public string m_strName; 
    public long m_lSize; 
    public DateTime m_dtLastModification; 
  } 
  public struct DirectoryEntry 
  { 
    public string m_strName; 
    public long m_lSize; 
    public DateTime m_dtLastModification; 
    public DirectoryEntry[] m_deArray; 
    public FileEntry[] m_feArray; 
  } 
  public interface IDirectoryLister 
  { 
    string DirectoryToList { get; set; } 
    
    bool Recursive { get; set; } 
    DirectoryEntry DirectoryEntry { get; } 
    bool BeginListing(); 
  } 
}

The FileEntry struct is an associated structure which is used to provide basic description of a file. The DirectoryEntry struct is another associated structure which is used to contain information on a directory found inside the target directory. DirectoryEntry contains a field named m_deArray which is an array of DirectoryEntry structs. It also contains a field named m_feArray which is an array of FileEntry structs. Hence, a single DirectoryEntry struct is a self-contained object containing references to the files and sub-folders of an entire directory tree.

The IDirectoryLister interface defines the required properties and methods of a directory listing object. The DirectoryToList string property is used to indicate (to a concrete implementation) the top-level directory to scan.

The Recursive bool property is used to indicate whether the file scanning operation is to be recursive or only confined to the target top-level directory.

The DirectoryEntry property is of type DirectoryEntry and is used by the client to obtain information on the top-level target directory. Once scanning has been performed, this property will contain all information on the files and sub-directories contained inside the target top-level directory.

The BeginListing() method simply tells the concrete implementation to begin the scanning process.

With this interface defined, we now begin with some concrete development work. First stop is the DirectoryListerBase class.

The DirectoryListerBase class

The DirectoryListerBase class is written in C#. It implements the IDirectoryLister interface described earlier. This base class serves to provide workhorse methods which may be used directly by a client or be inherited by specialized derived classes.

The source code for the DirectoryListerBase class is part of the DirectoryListerBase.sln solution which is included in this article's source code zip file. Once unzipped, it can be found in the following directory: <main directory>\DirectoryLister\Implementations\DirectoryListerBase, where <main directory> is where you have unzipped the files to. The main parts of the DirectoryListerBase class are listed below:

using System; 
using DirectoryListerInterfaceNamespace; 
using System.IO; 
namespace DirectoryListerBaseNamespace 
{ 
  public class DirectoryListerBase : IDirectoryLister 
  { 
    #region Code pertaining to internal member data. 
    string m_strDirectoryToList; 
    bool m_bRecursive; 
    DirectoryEntry m_deRoot; 
    #endregion 
    #region Code pertaining to constructors and destructors. 
    public DirectoryListerBase() 
    { 
      InitMemberData(); 
    } 
    #endregion 
    #region Code pertaining to IDirectoryLister interface implementations 
    public string DirectoryToList 
    { 
      get { return m_strDirectoryToList; } 
      set { m_strDirectoryToList = value; } 
    } 
    public bool Recursive 
    { 
      get { return m_bRecursive; } 
      set { m_bRecursive = value; } 
    } 
    public DirectoryEntry DirectoryEntry 
    { 
      get { return m_deRoot; } 
    } 
    public bool BeginListing() 
    { 
      if (m_strDirectoryToList == "") 
      { 
        return false; 
      } 
      m_deRoot.m_strName = m_strDirectoryToList; 
      m_deRoot.m_lSize = 0; 
      m_deRoot.m_dtLastModification 
        = Directory.GetLastWriteTime(m_strDirectoryToList); 
      GatherDirectories(m_strDirectoryToList, ref m_deRoot); 
      return true; 
    } 
    #endregion 
    ... 
    ... 
    ... 
}

I have listed mainly the interface-implementation parts of the DirectoryListerBase class so as to be able to discuss the private internal functions of the class separately.

The DirectoryToList string property is implemented simply by defining and using the m_strDirectoryToList string member data. The Recursive bool property is likewise implemented by the m_bRecursive bool member data.

The BeginListing() method is more interesting. It starts with checking the m_strDirectoryToList string member to see if it is empty. If so, it means that the DirectoryToList string property has not been set by the client application and so there is nothing to do but exit with a false value.

Assuming that the DirectoryToList string property has been set to a proper value, the next thing to do is to fill in a private DirectoryEntry struct named m_deRoot with as much data as possible. This member DirectoryEntry struct is then passed to the internal member function named GatherDirectories(). We shall study the GatherDirectories() function in greater detail below.

The GatherDirectories() method

The objective of GatherDirectories() is to scan through the contents of a specified directory (parameter 1) and store information on each file and sub-directory found inside this directory in a specified DirectoryEntry object (parameter 2). If the Recursive bool property of the DirectoryListerBase class is set to true, then each sub-directory is further examined individually and any files or sub-directories found within this sub-directory is processed further. This process is then repeated recursively until all the files and sub-directories of the specified (parameter 1) directory are exhaustively searched and processed.

The source code for the GatherDirectories() method is listed below:

protected virtual void GatherDirectories
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // First thing to do : gather all the files 

    // from the current directory. 

    GatherFiles(strDirectoryName, ref directory_entry_to_set); 
    string[] strSubdirectoryNames 
      = Directory.GetDirectories(strDirectoryName); 
    int iIndex = 0; 
     
    // Next, gather all the sub-directories inside 

    // the current directory. 

    directory_entry_to_set.m_deArray = 
               new DirectoryEntry[strSubdirectoryNames.Length]; 
    iIndex = 0; 
    foreach(string strSubdirectoryName in strSubdirectoryNames) 
    { 
      directory_entry_to_set.m_deArray[iIndex].m_strName = 
                                           strSubdirectoryName; 
      directory_entry_to_set.m_deArray[iIndex].m_lSize = 0; 
      directory_entry_to_set.m_deArray[iIndex].m_dtLastModification = 
                       Directory.GetLastWriteTime(strSubdirectoryName); 
      if (Recursive) 
      { 
        GatherDirectories
        (
          strSubdirectoryName, 
          ref directory_entry_to_set.m_deArray[iIndex]
        ); 
      } 
      iIndex++; 
    } 
  } 
  catch(Exception e) 
  { 
    System.Console.WriteLine("Exception Message : {0}", e.Message); 
    System.Console.WriteLine("Exception TargetSite : {0}", 
                                                     e.TargetSite); 
    System.Console.WriteLine("Exception Source : {0}", e.Source); 
  } 
}

To realize the objectives of GatherDirectories(), another internal method named GatherFiles() is used. We shall study the inner workings of GatherFiles() later on. For now, it suffices to note that GatherFiles() will scan through the files (no sub-directories are processed) contained inside a specified directory (its first parameter) and will store information pertaining to each discovered file inside individual FileEntry structs which are then stored inside the m_feArray member of a specified DirectoryEntry struct (its second parameter): GatherFiles(strDirectoryName, ref directory_entry_to_set);.

Hence the GatherDirectories() method invokes GatherFiles() in order to collate all the files contained within the current directory (strDirectoryName). Note that we will store information on all the found files in the m_feArray member of directory_entry_to_set.

GatherDirectories() then prepares an array of the sub-directories contained within the current directory strDirectoryName. This is accomplished by using the GetDirectories() method of the Directory class (defined in the System.IO namespace). The returned array of sub-directories is stored inside the local string array variable strSubdirectoryNames: string[] strSubdirectoryNames = Directory.GetDirectories(strDirectoryName);.

We then instantiate the DirectoryEntry array member m_deArray of the current DirectoryEntry struct directory_entry_to_set. The size of this array is determined by the array size of strSubdirectoryNames: directory_entry_to_set.m_deArray = new DirectoryEntry[strSubdirectoryNames.Length];.

We then iterate through each sub-directory name contained inside the strSubdirectoryNames array:

iIndex = 0; 
foreach(string strSubdirectoryName in strSubdirectoryNames) 
{ 
  directory_entry_to_set.m_deArray[iIndex].m_strName 
    = strSubdirectoryName; 
  directory_entry_to_set.m_deArray[iIndex].m_lSize = 0; 
  directory_entry_to_set.m_deArray[iIndex].m_dtLastModification 
    = Directory.GetLastWriteTime(strSubdirectoryName); 
  if (Recursive) 
  { 
    GatherDirectories
    (
      strSubdirectoryName, 
      ref directory_entry_to_set.m_deArray[iIndex]
    ); 
  } 
  iIndex++; 
}

Whatever information is available for each sub-directory (the name of which we know) is stored in a DirectoryEntry struct element of the DirectoryEntry array m_deArray of the current DirectoryEntry struct directory_entry_to_set.

Then, if the Recursive property is set to true, we perform a recursion by calling GatherDirectories() on the current sub-directory, passing along as parameter the current DirectoryEntry array element (directory_entry_to_set.m_deArray[iIndex]) designated for the sub-directory. This recursion will ensure an exhaustive searching and processing of all the files and sub-directories contained with any given top-level directory.

We will now move on to examine in detail the GatherFiles() method.

The GatherFiles() method

As mentioned previously, the GatherFiles() method scans through the files contained inside a specified directory (first parameter) and stores information about each discovered file in the m_feArray array of a specified DirectoryEntry struct (second parameter). This function is listed below:

protected virtual void GatherFiles
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // Get all files from given directory name. 

    string[] strFileNames 
      = Directory.GetFiles(strDirectoryName); 
    int iIndex = 0; 
    directory_entry_to_set.m_feArray 
      = new FileEntry[strFileNames.Length]; 
    foreach(string strFileName in strFileNames) 
    { 
      FileInfo fi = new FileInfo(strFileName); 
      (directory_entry_to_set.m_feArray)[iIndex].m_strName = 
                                                    strFileName; 
      (directory_entry_to_set.m_feArray)[iIndex].m_lSize = 
                                                      fi.Length; 
      (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification = 
                                                        fi.LastWriteTime; 
      iIndex++; 
    } 
  } 
  catch(Exception e) 
  { 
    System.Console.WriteLine("Exception Message : {0}", e.Message); 
    System.Console.WriteLine("Exception TargetSite : {0}", e.TargetSite); 
    System.Console.WriteLine("Exception Source : {0}", e.Source); 
  } 
}

GatherFiles() first prepares an array of names of all the files contained within the current directory strDirectoryName: string[] strFileNames = Directory.GetFiles(strDirectoryName);. The array of names is stored inside strFileNames which is an array of strings. The current FileEntry array member (m_feArray) of the current DirectoryEntry struct (directory_entry_to_set) is then instantiated: directory_entry_to_set.m_feArray = new FileEntry[strFileNames.Length];. The size of the array is set to the size of the strFileNames array. GatherFiles() then performs an iteration of all the file names contained inside the strFileNames array:

foreach(string strFileName in strFileNames) 
{ 
  FileInfo fi = new FileInfo(strFileName); 
  (directory_entry_to_set.m_feArray)[iIndex].m_strName 
    = strFileName; 
  (directory_entry_to_set.m_feArray)[iIndex].m_lSize = fi.Length; 
  (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
    = fi.LastWriteTime; 
  iIndex++; 
}

File information pertaining to each file is discovered through the use of the FileInfo class (defined in the System.IO namespace). The relevant data is then stored inside an element of the FileEntry array ((directory_entry_to_set.m_feArray)[iIndex]) of the current DirectoryEntry struct (directory_entry_to_set).

Final notes on the DirectoryListerBase class

The DirectoryListerBase class is a useful class by itself. It can be used generically in projects which require collection of files and sub-directories contained within any top-level directory. Also, note that both its GatherDirectories() and GatherFiles() methods have been declared as virtual functions. This means that any DirectoryListerBase-derived class may opt to override either of them if different algorithms for directory or file searching are required. We shall put this feature to good use later in the implementation of the AssemblyFilesLister class.

The AssemblyFilesLister class

The AssemblyFilesLister class is a C# written class which builds upon the functionality provided by the DirectoryListerBase class to provide a specialized IDirectoryLister interface implementation that focuses only on assembly files.

The source code for the AssemblyFilesLister class is part of the AssemblyFilesLister.sln solution which is included in this article's source code zip file. Once unzipped, it can be found in the following directory: <main directory>\DirectoryLister\Implementations\AssemblyFilesLister, where <main directory> is the location where you have unzipped the files.

A skeletal outline source listing of the AssemblyFilesLister class is listed below:

using System; 
using DirectoryListerInterfaceNamespace; 
using DirectoryListerBaseNamespace; 
using System.IO; 
namespace AssemblyFilesListerNamespace 
{ 
  public class AssemblyFilesLister : DirectoryListerBase 
  { 
    protected override void GatherFiles
    (
      string strDirectoryName, 
      ref DirectoryEntry directory_entry_to_set
    ) 
    { 
      ... 
      ... 
      ... 
    } 
    protected bool IsAssemblyFile(string strFileName) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

The most significant point about the AssemblyFilesLister class is that it is derived from DirectoryListerBase. Hence it inherits all the functionality of DirectoryListerBase including the IDirectoryLister interface implementation. The only thing for AssemblyFilesLister to do is to override the relevant virtual functions of its base class. There are only two virtual functions to override: GatherDirectories() and GatherFiles(). The functionality provided by DirectoryListerBase.GatherDirectories() is sufficient for AssemblyFilesLister. There is no need for any modifications here. DirectoryListerBase.GatherFiles(), however, requires further fine-tuning. This base implementation discovers all the files within a directory. We only need to gather the assembly files. We thus override GatherFiles() and provide a custom one for AssemblyFilesLister.

AssemblyFilesLister.GatherFiles()

The source listing for AssemblyFilesLister.GatherFiles() is listed below:

protected override void GatherFiles
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  // Get all files from given directory name. 

  string[] strFileNames = Directory.GetFiles(strDirectoryName); 
  int iCountOfAssemblies = 0; 
  int iIndex = 0; 
  // First determine how many assembly files there are 

  // in this current directory. 

  foreach(string strFileName in strFileNames) 
  { 
    if (IsAssemblyFile(strFileName)) 
    { 
      iCountOfAssemblies++; 
    } 
  } 
  if (iCountOfAssemblies > 0) 
  { 
    directory_entry_to_set.m_feArray 
      = new FileEntry[iCountOfAssemblies]; 
    foreach(string strFileName in strFileNames) 
    { 
      if (IsAssemblyFile(strFileName)) 
      { 
        FileInfo fi = new FileInfo(strFileName); 
        (directory_entry_to_set.m_feArray)[iIndex].m_strName 
          = strFileName;  
        (directory_entry_to_set.m_feArray)[iIndex].m_lSize 
          = fi.Length; 
        (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
          = fi.LastWriteTime; 
        iIndex++; 
      } 
    } 
  } 
}

Just like its base-class counterpart, AssemblyFilesLister.GatherFiles() uses the GetFiles() method of the Directory class to obtain an array of names of all the files contained within its specified directory strDirectoryName (first parameter): string[] strFileNames = Directory.GetFiles(strDirectoryName);.

The file names are stored in the strFileNames string array. Now because strFileNames contains the names of all the files inside strDirectoryName, we cannot use its array size to instantiate the FileEntry array member m_feArray of the specified DirectoryEntry struct directory_entry_to_set.

Instead, we need to iterate through the file names in the strFileNames array to determine how many of these files are actually assembly files:

// First determine how many assembly files there are in this current 

// directory. 

foreach(string strFileName in strFileNames) 
{ 
  if (IsAssemblyFile(strFileName)) 
  { 
    iCountOfAssemblies++; 
  } 
}

This is accomplished by the internal IsAssemblyFile() method. We shall study the IsAssemblyFile() method in more detail later on. For now, just note that it returns a bool that indicates whether an input file name is actually an assembly file. If a file name in the strFileNames array is an assembly file, we increase an internal counter iCountOfAssemblies. At the end of the iteration, this counter will tell us how many assembly files exist within the current directory. If this count is greater than zero, we proceed to instantiate directory_entry_to_set.m_feArray and repeat the same iteration performed previously but this time, instead of simply counting the occurrences of assembly files, we store the file information of each discovered assembly file:

if (iCountOfAssemblies > 0) 
{ 
    directory_entry_to_set.m_feArray = 
                         new FileEntry[iCountOfAssemblies]; 
    foreach(string strFileName in strFileNames) 
    { 
      if (IsAssemblyFile(strFileName)) 
      { 
        FileInfo fi = new FileInfo(strFileName); 
        (directory_entry_to_set.m_feArray)[iIndex].m_strName 
          = strFileName;  
        (directory_entry_to_set.m_feArray)[iIndex].m_lSize 
          = fi.Length; 
        (directory_entry_to_set.m_feArray)[iIndex].m_dtLastModification 
          = fi.LastWriteTime; 
        iIndex++; 
      } 
    } 
}

Having provided its own version of GatherFiles(), when GatherDirectories() is called on AssemblyFilesLister, at the point where GatherFiles() is called, the AssemblyFilesLister.GatherFiles() version will be called:

protected virtual void GatherDirectories
(
  string strDirectoryName, 
  ref DirectoryEntry directory_entry_to_set
) 
{ 
  try 
  { 
    // First thing to do : gather all the files 

    // from the current directory. 

    // AssemblyFilesLister.GatherFiles() will be 

    // called below. 

    GatherFiles(strDirectoryName, ref directory_entry_to_set); 
    ... 
    ... 
    ...

IsAssemblyFile

The IsAssemblyFile() method is listed below:

protected bool IsAssemblyFile(string strFileName) 
{ 
  bool bRet = false; 
  try 
  { 
    System.Reflection.AssemblyName testAssembly = 
      System.Reflection.AssemblyName.GetAssemblyName(strFileName); 
    bRet = true; 
  } 
  catch (System.IO.FileNotFoundException) 
  { 
    bRet = false; 
  } 
  catch (System.BadImageFormatException) 
  { 
    bRet = false; 
  } 
  catch (System.IO.FileLoadException) 
  { 
    bRet = false; 
  } 
  
  return bRet; 
}

As mentioned above, IsAssemblyFile() is specified to return a bool indicating whether an input file name (parameter strFileName) is an assembly file. The code is taken directly from the code sample of the MSDN Online C# Programmer's Reference under the title "How to: Determine If a File Is an Assembly (C# Programming Guide)".

Essentially, to programmatically determine whether a file is an assembly, we can call the System.Reflection.AssemblyName.GetAssemblyName() method, passing the full file path and the name of the file we are testing. If a BadImageFormatException exception is thrown, the file is not an assembly. This simple principle is used in the IsAssemblyFile() method.

Final notes on the AssemblyFilesLister class

The AssemblyFilesLister class is the class that will be used by the eventual KeywordAttributeAssemblyLister application. What we have achieved with the AssemblyFilesLister class is a complete IDirectoryLister interface implementation that is based on the complete functionality provided by the DirectoryListerBase class with special focus on assembly file searching.

What we need to build next is an even finer filtering tool that is able to select only assembly files which contain types that are attributed by the KeywordAttribute that is parameterized by a specific keyword string. This is accomplished in the KeywordAttributeAssemblyLister application which we shall examine next.

The KeywordAttributeAssemblyLister application

Finally, we have reached the KeywordAttributeAssemblyLister application. The KeywordAttributeAssemblyLister is a C# written application. It uses the functions exposed by the IDirectoryLister interface to search a specified top-level directory for specific assemblies that contain types that are attributed by the KeywordAttribute attributes that are parameterized by a specific keyword string and a minimum relevance level. The search can be recursive or non-recursive.

The source codes for the KeywordAttributeAssemblyLister application is included in this article's source code zip file. Once unzipped, it can be found in the following directory: <main directory>\DirectoryLister\Clients\KeywordAttributeAssemblyLister, where <main directory> is the location where you have unzipped the files.

The KeywordAttributeAssemblyLister application is contained within the KeywordAttributeAssemblyLister namespace which is contained in the ClassMain.cs source file. We now analyze carefully each of the entities defined within the KeywordAttributeAssemblyLister namespace:

The KeywordAttributedType struct

The KeywordAttributedType struct is used to contain information about a type that is attributed by a specific KeywordAttribute. That is, in the source code, the type has been attributed with something like the following: Keyword("some_keyword", some_relevance_value).

The struct is defined as follows:

public struct KeywordAttributedType 
{ 
  public string m_TypeName; 
  public int m_iRelevance; 
}

where m_TypeName is a string that indicates the name of the type and m_iRelevance indicates the relevance value of the keyword associated with the type.

The KeywordAttributedAssembly struct

The KeywordAttributedAssembly struct is used to contain information about an assembly that contains some type that is attributed by a specific KeywordAttribute. This struct is defined as follows:

public struct KeywordAttributedAssembly 
{ 
  public string m_strAssemblyFullPath; 
  public string m_strKeyword; 
  public ArrayList m_KeywordAttributedTypesArray; 
}

where m_strAssemblyFullPath is the full path to the assembly file. m_strKeyword is the keyword used in the KeywordAttribute. m_KeywordAttributedTypesArray is an ArrayList that contains KeywordAttributedType structs.

The basic idea behind these two structs is that eventually, when time comes to display the expected data, we must have an array of KeywordAttributedAssembly structs. Each KeywordAttributedAssembly struct will represent an assembly file which will contain types that are attributed by a KeywordAttribute parameterized by a specific keyword string. Its KeywordAttributedType ArrayList will hold all the keyword attributed type names together with each type's relevance value.

The ClassMain class

ClassMain is the main class of the entire KeywordAttributeAssemblyLister application. Its static Main() function is the entry point of the application. Main() pretty much delivers the required functionality of the entire application with the help of two additional internal methods SearchAndCollectKeywordAttributesAssemblies() and CollectKeywordAttributedTypes().

The Main() method

The source code of the Main() function is listed below:

static void Main(string[] args) 
{ 
  // We make use of the services of an IDirectoryLister 

  // interface implemented 

  // by the AssemblyFilesLister class. 

  IDirectoryLister dl = new AssemblyFilesLister(); 
  // The "dl" DirectoryLister object will return to us a 

  // DirectoryEntry object filled with sub-directories 

  // and files contained inside a target directory. 

  DirectoryEntry de; 
  // KeywordAttributedAssembliesArray is an ArrayList object 

  // which holds an array of KeywordAttributedAssembly entries. 

  // Each entry represents an assembly that uses 

  // the specified Keyword Attribute. 

  ArrayList KeywordAttributedAssembliesArray = new ArrayList(); 
  int i = 0; 
  // Indicate to the IDirectoryLister object which directory 

  // to scan. 

  // In our case, this will be the directory supplied by the user in 

  // the first argument to this program. 

  dl.DirectoryToList = args[0]; 
  // Indicate that we want the directory scanning to be recursive. 

  dl.Recursive = true; 
  // Tell the IDirectoryLister object to begin scanning. 

  dl.BeginListing(); 
  // At this point, directory scanning is done and we obtain 

  // the main directory entry object. 

  de = dl.DirectoryEntry; 
  Console.WriteLine
  (
    "Searching [{0}] for Assemblies Attributed with Keyword \"{1}\" 
                                        and Minimum Relevance {2}.", 
                                        args[0], 
                                        args[1], 
                                        args[2]
  ); 
  Console.WriteLine(); 
  // Search through the main directory and all its subdirectories 

  // and scan for Keyword Attributed Assemblies. 

  SearchAndCollectKeywordAttributesAssemblies
  (
      de, 
      args[1], 
      Convert.ToInt32(args[2]), 
      ref KeywordAttributedAssembliesArray
  ); 
  // Once done, KeywordAttributedAssembliesArray will contain 

  // KeywordAttributedAssembly objects each of which contains 

  // information on one assembly that uses the specified 

  // Keyword Attribute. 

  for (i = 0; i < KeywordAttributedAssembliesArray.Count; i++) 
  { 
    KeywordAttributedAssembly keyword_attributed_assembly = 
      (KeywordAttributedAssembly)(KeywordAttributedAssembliesArray[i]);
    Console.WriteLine
    (
      "Listing Types in Assembly : [{0}]", 
      keyword_attributed_assembly.m_strAssemblyFullPath
    ); 
    // Each keyword_attributed_assembly contains an internal array 

    // of KeywordAttributedType objects. Each object contains 

    // information on a type that uses the specified Keyword 

    // Attribute. The relevance level is also included. 

    int j = 0; 
    for 
    (
       j = 0; 
       j < keyword_attributed_assembly.m_KeywordAttributedTypesArray.Count; 
       j++
    ) 
    { 
        KeywordAttributedType keyword_attributed_type = 
                                               (KeywordAttributedType)
        (keyword_attributed_assembly.m_KeywordAttributedTypesArray[j]); 
      
        Console.WriteLine("Type Name : {0}. Relevance : {1}.", 
                              keyword_attributed_type.m_TypeName, 
                              keyword_attributed_type.m_iRelevance); 
    } 
  
    Console.WriteLine(); 
  } 
}

The Main() method uses the services of the IDirectoryLister interface as implemented by the AssemblyFilesLister class:

// We make use of the services of an IDirectoryLister 

// interface implemented 

// by the AssemblyFilesLister class. 

IDirectoryLister dl = new AssemblyFilesLister(); 
// The "dl" DirectoryLister object will return to us 

// a DirectoryEntry object filled with sub-directories 

// and files contained inside a target directory. 

DirectoryEntry de;

Main() then instructs the directory lister object to recursively scan through a specified top-level directory (first command line argument) to collect all the assembly files contained within the directory:

  // Indicate to the IDirectoryLister object which directory 

  // to scan. In our case, this will be the directory supplied 

  // by the user in the first argument to this program. 

  dl.DirectoryToList = args[0]; 
  // Indicate that we want the directory scanning 

  // to be recursive. 

  dl.Recursive = true; 
  // Tell the IDirectoryLister object to begin scanning. 

  dl.BeginListing();

Once the collection of all files has been prepared, the DirectoryEntry member of the IDirectoryLister interface implementation will hold a tree of all the assemblies contained within the specified top-level directory (args[0]):

  // At this point, directory scanning is done 

  // and we obtain the main directory entry object. 

  de = dl.DirectoryEntry;

This tree of assemblies requires further filtering. We need to pick out the assemblies which are attributed by the KeywordAttribute attributes that are parameterized by a specific keyword string and a relevance value above a certain minimum. This is achieved by the SearchAndCollectKeywordAttributesAssemblies() method:

  // Search through the main directory and all its 

  // subdirectories and scan for 

  // Keyword Attributed Assemblies. 

  SearchAndCollectKeywordAttributesAssemblies
  (
    de, 
    args[1], 
    Convert.ToInt32(args[2]), 
    ref KeywordAttributedAssembliesArray
  );

When SearchAndCollectKeywordAttributesAssemblies() completes, the ArrayList object KeywordAttributedAssembliesArray will contain KeywordAttributedAssembly structs.Each struct will contain the full path of a relevant assembly, the required keyword string, and an ArrayList of KeywordAttributedType structs. Each KeywordAttributedType struct will contain the name of a type which has been attributed by a KeywordAttribute attribute which is parameterized by a specific keyword string and a specific relevance value. We then iterate through these final assembly files and display all the relevant information.

The SearchAndCollectKeywordAttributesAssemblies() method

The source code of the SearchAndCollectKeywordAttributesAssemblies() method is listed below:

static void SearchAndCollectKeywordAttributesAssemblies
(
  DirectoryEntry de, 
  string strKeyword, 
  int iMinRelevance, 
  ref ArrayList KeywordAttributedAssembliesArray
) 
{ 
  if (de.m_feArray != null) 
  { 
    foreach(FileEntry fe in de.m_feArray) 
    { 
      CollectKeywordAttributedTypes
      (
        fe.m_strName, 
        strKeyword, 
        iMinRelevance, 
        ref KeywordAttributedAssembliesArray
      ); 
    } 
  } 
  if (de.m_deArray != null) 
  { 
    foreach(DirectoryEntry _de in de.m_deArray) 
    { 
      SearchAndCollectKeywordAttributesAssemblies
      (
        _de, 
        strKeyword, 
        iMinRelevance, 
        ref KeywordAttributedAssembliesArray
      ); 
    } 
  } 
}

It is a simple function. It begins by iterating through the files contained in the m_feArray of the input DirectoryEntry struct (de). Each file in this array is an assembly file. For each assembly file, we call the CollectKeywordAttributedTypes() to process it further. We shall investigate CollectKeywordAttributedTypes() in more detail later on. For now, just note that it processes an assembly file (whose name is passed as parameter) to obtain information on the types (defined in the assembly) which are attributed by the KeywordAttribute attribute parameterized by a specific keyword string and a relevance value above a certain minimum.

Do not forget that the input DirectoryEntry struct de will also contain an array of sub-directories (i.e. m_deArray). The SearchAndCollectKeywordAttributesAssemblies() method will recursively call itself on each and every sub-directory in this array.

The CollectKeywordAttributedTypes() method

The CollectKeywordAttributedTypes() method loads an assembly file using the LoadFrom() method of the Assembly class:

static void CollectKeywordAttributedTypes
(
  string strAssembly, 
  string strKeyword, 
  int iMinRelevance, 
  ref ArrayList KeywordAttributedAssembliesArray
) 
{ 
  Assembly assembly; 
  try 
  { 
    // Load the assembly from a filename 

    assembly = Assembly.LoadFrom(strAssembly); 
  } 
  catch (Exception) 
  { 
    // Could not load the assembly. 

    return; 
  } 
  ... 
  ... 
  ...

The loaded assembly is represented by an Assembly class instance named "assembly". We then define a KeywordAttributedAssembly struct variable as well as a boolean variable bAddToKeywordAttributedAssembliesArray:

KeywordAttributedAssembly current_keyword_attributed_assembly 
                               = new KeywordAttributedAssembly(); 
bool bAddToKeywordAttributedAssembliesArray = false;

The KeywordAttributedAssembly struct variable current_keyword_attributed_assembly is used to hold information on the current assembly file that we have just loaded and are processing. We will fill it with whatever available information we have:

// Prepare a new AssemblyWithKeyword object in case we are indeed 

// currently working with an Assembly that uses the Keyword attribute. 

current_keyword_attributed_assembly.m_strAssemblyFullPath = strAssembly; 
current_keyword_attributed_assembly.m_strKeyword = strKeyword; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray = 
                                                        new ArrayList();

However, its usefulness is realized only when the bool variable bAddToKeywordAttributedAssembliesArray is set to true. bAddToKeywordAttributedAssembliesArray will be true only if the current assembly contains types that are attributed by the KeywordAttribute attributes which are parameterized by a specified keyword string and a relevance value that is equal to or above a certain minimum. If this is the case, we will add current_keyword_attributed_assembly to the input KeywordAttributedAssembliesArray ArrayList so that when CollectKeywordAttributedTypes() returns, the caller will have a relevant list of assemblies each of which fits the requirement. We shall see this in action later on.

We next attempt to extract all the custom attributes attached to the current assembly (at the "assembly" level) which are of type KeywordAttribute:

// Collect all the Keyword Attrbutes from the Assembly itself. 

// Check for the KeywordAttribute on the Assembly itself. 

KeywordAttribute[] keyword_attributes_in_assembly 
  = (KeywordAttribute[])
    (assembly.GetCustomAttributes(typeof(KeywordAttribute), true));

The extracted KeywordAttribute attributes (if any) are stored inside the KeywordAttribute array keyword_attributes_in_assembly.

Recall from earlier discussion in the section "The KeywordAttribute Custom Attribute" that the KeywordAttribute assembly is strong named and is made a shared resource and registered in the GAC. Here is where the benefit of this arrangement is realized: if the KeywordAttribute assembly that the KeywordAttributeAssemblyLister application is referring to and that which the current loaded assembly is referring to are not the same, then the above call to GetCustomAttributes(), with the first parameter being typeof(KeywordAttribute), will return an empty array. In fact, all calls to GetCustomAttributes() of this nature will yield empty arrays.

This happens when KeywordAttributeAssemblyLister loads one KeywordAttribute assembly while the current assembly (being examined) loads another. Recall that a .NET type's identity is tied with its containing assembly. To the .NET runtime, the KeywordAttribute type used by KeywordAttributeAssemblyLister is not the same as that used by the current loaded assembly by virtue of the fact that two separate containing assemblies are involved. By ensuring that the KeywordAttribute assembly is registered to the GAC, hence making it a shared dependency for KeywordAttributeAssemblyLister and all assemblies it loads, we avoid this problem.

Now, if the returned array keyword_attributes_in_assembly is non-zero in size, it means that the assembly does have associated attributes of type KeywordAttribute. This means that the assembly uses at least one KeywordAttribute at the "assembly" level, e.g.: [assembly: Keyword("test", 5)]

However, having associated attributes of type KeywordAttribute does not qualify an assembly to be listed by our application. Here is where we ensure that any discovered KeywordAttribute attribute is parameterized by a specific keyword string and a relevance value equal to or above a specified minimum:

if (keyword_attributes_in_assembly.Length > 0) 
{ 
  // We enumerate through the Keyword Attributes in the assembly. 

  foreach (KeywordAttribute a in keyword_attributes_in_assembly) 
  {    
    // We filter through the keyword attributes and select 

    // only those whose keywords match the search keyword (strKeyword) 

    // and whose relevance is at least equal to the required 

    // relevance (i.e. iMinRelevance). 

    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      KeywordAttributedType keyword_attributed_type 
        = new KeywordAttributedType(); 
      keyword_attributed_type.m_TypeName = "assembly"; 
      keyword_attributed_type.m_iRelevance = a.Relevance; 
      // Add the current TypeWithKeyword object to the ArrayList 

      // of TypeWithKeyword objects in the Current AssemblyWithKeyword 

      // object (current_assembly_with_keyword). 

      current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
      ((object)keyword_attributed_type); 
      // Indicate that the Current AssemblyWithKeyword object 

      // (current_assembly_with_keyword) 

      // is to be added to the input reference 

      // ArrayList AssemblyWithKeywordArray later on.   

      bAddToKeywordAttributedAssembliesArray = true; 
    } 
  } 
}

We iterate through the elements in keyword_attributes_in_assembly and for each KeywordAttribute attribute, we check that its keyword string matches that of the input strKeyword parameter and its relevance value is equal to or is above the input iMinRelevance parameter. If so, we have just found a type (the assembly itself, actually) that is attributed by a matching KeywordAttribute attribute.

We thus create a KeywordAttributedType struct, fill it with the appropriate values, and then add it to the KeywordAttributedType array m_KeywordAttributedTypesArray of current_keyword_attributed_assembly. We also set the bool bAddToKeywordAttributedAssembliesArray to true. The effect of setting this bool variable to true will be discussed later.

Next, we perform a large iteration and go through all the types defined inside the assembly. For each type, we will perform a series of operations to extract the custom attributes (specifically of type KeywordAttribute) contained in the type itself, contained in the members of the type, and if the member happens to be a method, contained in any of the parameters of the method:

foreach (Type t in assembly.GetTypes()) 
{  
  // Check for the KeywordAttribute on the type itself. 

  KeywordAttribute[] keyword_attributes_in_types 
    = (KeywordAttribute[])
      (t.GetCustomAttributes(typeof(KeywordAttribute), true)); 
  
  if (keyword_attributes_in_types.Length > 0) 
  { 
    ... 
  } 
  // Enumerate over the members of the current type 

  foreach (MemberInfo member in t.GetMembers()) 
  { 
    // Check for the KeywordAttribute on the member 

    KeywordAttribute[] keyword_attributes_in_type_members 
      = (KeywordAttribute [])
         (member.GetCustomAttributes(typeof(KeywordAttribute), true)); 
    if (keyword_attributes_in_type_members.Length > 0) 
    { 
      ... 
    } 
    // If the current member is a method, we determine if any of the 

    // method's parameters is also Keyword Attributed. 

    if (member.MemberType == MemberTypes.Method) 
    { 
     ... 
    } 
  } 
}

Collecting KeywordAttribute attributes contained in the type itself

We start by obtaining the custom attributes (specifically of type KeywordAttribute) which are associated with the type itself. These KeywordAttribute attributes (if any) are stored in the KeywordAttribute array keyword_attributes_in_types:

// Check for the KeywordAttribute on the type 

KeywordAttribute[] keyword_attributes_in_types 
  = (KeywordAttribute[])
    (t.GetCustomAttributes(typeof(KeywordAttribute), true));

If this array is non-zero in size, we iterate through each of the KeywordAttribute attributes contained in the array and select only those which are parameterized by the specific keyword string (input parameter strKeyword) and by a relevance value above or equal to the minimum value (input parameter iMinRelevance):

if (keyword_attributes_in_types.Length > 0) 
{ 
  foreach (KeywordAttribute a in keyword_attributes_in_types) 
  { 
    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      KeywordAttributedType keyword_attributed_type = 
                                       new KeywordAttributedType(); 
      keyword_attributed_type.m_TypeName = t.ToString(); 
      keyword_attributed_type.m_iRelevance = a.Relevance; 
      current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                           ((object)keyword_attributed_type); 
      bAddToKeywordAttributedAssembliesArray = true; 
    } 
  } 
}

Just as we did for the assembly-level KeywordAttribute, for each selected KeywordAttribute, we create a KeywordAttributedType struct keyword_attributed_type. We then assign its m_TypeName field to the string form of the current type and assign its m_iRelevance field to the relevance value of the current KeywordAttribute. An example of such a KeywordAttribute attributed type is listed below:

[Keyword("test", 7)] 
public class KeywordAttributedClass01 
{ 
   ... 
   ... 
   ... 
}

In the above example, the KeywordAttributedClass01 class is an example type which is attributed by the KeywordAttribute attribute parameterized by "test" and a relevance value of 7. We also add this keyword_attributed_type struct to the KeywordAttributedType array member m_KeywordAttributedTypesArray of the KeywordAttributedAssembly struct current_keyword_attributed_assembly created earlier.

Finally, we set the bool variable bAddToKeywordAttributedAssembliesArray to true. The effect of setting this bool variable to true will be discussed later.

Collecting KeywordAttribute attributes contained in the members of the type

Note that we are still iterating through the types defined inside the current assembly. We have just processed the relevant KeywordAttribute attributes associated with the current type. Next we will iterate through all the members of the current type:

// Enumerate over the members of the current type 

foreach (MemberInfo member in t.GetMembers()) 
{ 
   ... 
   ... 
   ... 
}

We first collect all the KeywordAttribute custom attributes associated with the current member into an array of KeywordAttribute keyword_attributes_in_type_members:

// Check for the KeywordAttribute on the member 

KeywordAttribute[] keyword_attributes_in_type_members 
  = (KeywordAttribute [])
    (member.GetCustomAttributes(typeof(KeywordAttribute), true));

Then, as usual, if this array is non-zero in size, it means that the current member does use at least one KeywordAttribute attribute. We then iterate through each KeywordAttribute attribute and select only those which are parameterized by the specific keyword string strKeyword and a relevance value equal to or above the minimum value iMinRelevance:

if (keyword_attributes_in_type_members.Length > 0) 
{ 
  foreach (KeywordAttribute a in keyword_attributes_in_type_members) 
  { 
    if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

As usual, for each relevant KeywordAttribute attribute, we create a KeywordAttributedType struct, fill it with the relevant available information and then add it to the m_KeywordAttributedTypesArray array of current_keyword_attributed_assembly:

KeywordAttributedType keyword_attributed_type = 
                                         new KeywordAttributedType(); 
keyword_attributed_type.m_TypeName = t.ToString() + "." + member.Name; 
keyword_attributed_type.m_iRelevance = a.Relevance; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                     ((object)keyword_attributed_type); 
bAddToKeywordAttributedAssembliesArray = true;

Finally, we set bAddToKeywordAttributedAssembliesArray to true. Note that bAddToKeywordAttributedAssembliesArray may already have been set to true but this does not matter.

Collecting KeywordAttribute attributes contained in the parameters of method members of the type

Now, while processing each member contained in the type, we determine if the current member is a Method. If so, we must process the parameters of the method and determine if any parameter is attributed by the KeywordAttribute:

if (member.MemberType == MemberTypes.Method) 
{ 
  MethodInfo mi = (MethodInfo)member; 
  foreach(ParameterInfo p in mi.GetParameters()) 
  { 
    KeywordAttribute[] keyword_attributes_in_method_parameters = 
                                               (KeywordAttribute [])
        (p.GetCustomAttributes(typeof(KeywordAttribute), true)); 
    if (keyword_attributes_in_method_parameters.Length > 0) 
    { 
      ... 
      ... 
      ... 
    } 
  } 
}

For each method parameter which has been attributed by the KeywordAttribute attribute, the KeywordAttribute array keyword_attributes_in_method_parameters will be of non-zero size. We process each of these and select only those which are parameterized by the specific keyword string strKeyword and relevance value greater than or equal to iMinRelevance:

foreach (KeywordAttribute a in keyword_attributes_in_method_parameters) 
{ 
  if ((a.Keyword == strKeyword) && (a.Relevance >= iMinRelevance)) 
  { 
    ... 
    ... 
    ... 
  } 
}

And as usual, for each relevant KeywordAttribute attribute, we will create a KeywordAttributeType struct, fill it with the available information and add it to the m_KeywordAttributedTypesArray array member of current_keyword_attributed_assembly:

KeywordAttributedType keyword_attributed_type = 
                                new KeywordAttributedType(); 
keyword_attributed_type.m_TypeName = "Parameter to Method " 
                                               + member.Name 
                                               + "() : " 
                                               + p.Name; 
keyword_attributed_type.m_iRelevance = a.Relevance; 
current_keyword_attributed_assembly.m_KeywordAttributedTypesArray.Add
                                     ((object)keyword_attributed_type); 
bAddToKeywordAttributedAssembliesArray = true;

Finally the bool variable bAddToKeywordAttributedAssembliesArray is set to true.

Adding current_keyword_attributed_assembly to the input KeywordAttributedAssembliesArray

If the bool variable bAddToKeywordAttributedAssembliesArray is set to true, the earlier created KeywordAttributedAssembly struct current_keyword_attributed_assembly is added to the input KeywordAttributedAssembliesArray which is supplied by the caller of the CollectKeywordAttributedTypes() method by reference:

if (bAddToKeywordAttributedAssembliesArray) 
{ 
  KeywordAttributedAssembliesArray.Add
  ((object)current_keyword_attributed_assembly); 
}

Recall that the previously defined KeywordAttributedAssembly struct current_keyword_attributed_assembly was created to hold information on the current assembly being processed. We mentioned that current_keyword_attributed_assembly will not be useful unless at least one of its types is attributed by the KeywordAttribute which is parameterized by a specific keyword string and a relevance level equal to or above a specific minimum value. We mentioned that if this is indeed the case, bAddToKeywordAttributedAssembliesArray will be set to true.

Hence, at the end of the CollectKeywordAttributedTypes() method, the caller's original ArrayList KeywordAttributedAssembliesArray will either have one more KeywordAttributedAssembly struct added or not at all.

Building the source code

Once you have downloaded the source code zip file KeywordAttributeSystem_src.zip, unzip it to a folder of your choice. Next, the sequence of building is important. Please follow the following guidelines during compilation:

  • Build the KeywordAttribute assembly first and register it to the GAC.

    The KeywordAttribute project (KeywordAttribute.sln) should be built first. This will be contained in the following directory: <main directory>\KeywordAttribute, where <main directory> is the location where you have unzipped the files. Thereafter, register the resultant KeywordAttribute.dll into the Global Assembly Cache by invoking the batch file RegisterAssemblyToGAC.bat contained within the project folder.

  • Build the DirectoryListerInterface project.

    The DirectoryListerInterface.sln project should be the next one to be compiled. This will be contained in the following directory: <main directory>\DirectoryLister\Interfaces\DirectoryListerInterface, where <main directory> is the location where you have unzipped the files. The resultant assembly DirectoryListerInterface.dll can then be referenced by the implementation code which we shall compile next.

  • Build the DirectoryListerBase project.

    The DirectoryListerBase.sln project should then be built. This will be contained in the following directory: <main directory>\DirectoryLister\Implementations\DirectoryListerBase, where <main directory> is the location where you have unzipped the files. The resultant assembly DirectoryListerBase.dll can then be referenced by the other implementation project AssemblyFilesLister.sln.

  • Build the AssemblyFilesLister project.

    The AssemblyFilesLister.sln project should be built next. This will be contained in the following directory: <main directory>\DirectoryLister\Implementations\AssemblyFilesLister, where <main directory> is the location where you have unzipped the files. The resultant assembly AssemblyFilesLister.dll can then be referenced by the KeywordAttributeAssemblyLister project.

  • Build the KeywordAttributeAssemblyLister project.

    The KeywordAttributeAssemblyLister.sln project should now be compiled. This will be contained in the following directory: <main directory>\DirectoryLister\Clients\KeywordAttributeAssemblyLister, where <main directory> is the location where you have unzipped the files. Note that this project's reference to the KeywordAttribute assembly should be such that the "Copy Local" property is set to "False". This is very important. This will ensure that at runtime, the KeywordAttribute assembly as registered in the GAC (instead of a local copy) is loaded and used.

  • Build the test assemblies.

    I have prepared several test assembly projects under the following directory: <main directory>\DirectoryLister\Clients\KeywordAttributedAssemblies, where <main directory> is where you have unzipped the files. These projects produce test assemblies each of which contain various types which use the KeywordAttribute attribute. All the test assemblies are strong named so that they can be registered to the GAC.

    The test assemblies can now be built one by one. Just like the KeywordAttributeAssemblyLister.sln project, each test assembly's reference to the KeywordAttribute assembly should be such that the "Copy Local" property is set to "False".

    This way, at runtime, when KeywordAttributeAssemblyLister.exe loads each test assembly, the KeywordAttribute assembly (which is a common dependency assembly) as registered in the GAC (instead of a local copies) is loaded and used.

Testing the KeywordAttributeAssemblyLister application

The KeywordAttributeAssemblyLister application takes in three command line arguments:

  • Argument 1: full path to the top-level directory to scan for assemblies.
  • Argument 2: the keyword string.
  • Argument 3: minimum relevance level.

Hence to scan the directory "c:\MyAssemblies" for assemblies which contain types that are attributed by the KeywordAttribute attribute with keyword string "test" and minimum relevance value of 3, we will type the following in the command line: KeywordAttributeAssemblyLister.exe c:\MyAssemblies test 3.

The diagram below shows the result of a sample search for the keyword "test" and minimum relevance value 8 using my test assemblies which are contained in the directory: D:\Limbl\Temp\DirectoryLister\Clients\KeywordAttributedAssemblies:

To scan the GAC, pass the full path to the GAC as the first parameter to KeywordAttributeAssemblyLister application, e.g. "C:\WINDOWS\assembly".

Conclusion

We have demonstrated a very useful example of a custom attribute which is commercially viable, thanks to Adam Nathan. There are a lot of possible ways to enhance the KeywordAttributeAssemblyLister application including transforming it to a class library or turning it to a remoting server that can serve the functionality of returning a list of relevant KeywordAttribute attributed assemblies stored in the GAC of a target machine.

For a .NET beginner, I hope that this article has demonstrated how easy it is to create custom attributes and to exploit the versatility of Reflection. As mentioned, the DirectoryListerBase and AssemblyFilesLister classes are very useful classes by themselves which can be used in various projects.

I do hope that you have benefited from this article. Please do not hesitate to contact me should you find any errors in the explanatory text or any bugs in the source code or if you simply need to clarify any issue with me regarding this article.

Acknowledgements and references

  • Professional C# by Simon Robinson, Ollie Cornes, Jay Glynn, Buston Harvey, Craig McQueen, Jerod Moemeka, Christian Nagel, Morgan Skinner, Karli Watson. Published by Wrox Press.
  • .NET and COM The Complete Interoperability Guide by Adam Nathan. Published by SAMS Publishing. The source codes for the KeywordAttribute attribute is taken from Adam Nathan's book.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here